/**
 * Copyright (C) 2011-2012 The XDocReport Team <xdocreport@googlegroups.com>
 *
 * All rights reserved.
 *
 * Permission is hereby granted, free  of charge, to any person obtaining
 * a  copy  of this  software  and  associated  documentation files  (the
 * "Software"), to  deal in  the Software without  restriction, including
 * without limitation  the rights to  use, copy, modify,  merge, publish,
 * distribute,  sublicense, and/or sell  copies of  the Software,  and to
 * permit persons to whom the Software  is furnished to do so, subject to
 * the following conditions:
 *
 * The  above  copyright  notice  and  this permission  notice  shall  be
 * included in all copies or substantial portions of the Software.
 *
 * THE  SOFTWARE IS  PROVIDED  "AS  IS", WITHOUT  WARRANTY  OF ANY  KIND,
 * EXPRESS OR  IMPLIED, INCLUDING  BUT NOT LIMITED  TO THE  WARRANTIES OF
 * MERCHANTABILITY,    FITNESS    FOR    A   PARTICULAR    PURPOSE    AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE,  ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package fr.opensagres.xdocreport.document.preprocessor.sax;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.xml.sax.Attributes;

/**
 * Buffered element which stores start Tag element (ex: <a>) and end Tag element (ex: </a>). The start Tag element
 * stores too the {@link BufferedElement} children.
 */
public class BufferedElement
    implements IBufferedRegion
{

    private final BufferedElement parent;

    private final String name;

    private final String startTagElementName;

    private final String endTagElementName;

    protected final BufferedStartTagElement startTagElement;

    protected final BufferedEndTagElement endTagElement;

    private BufferedTagElement currentTagElement;

    private final Attributes attributes;

    private Collection<BufferedAttribute> dynamicAttributes = null;

    private boolean reseted;

    private Map<String, String> data;

    public BufferedElement( BufferedElement parent, String uri, String localName, String name, Attributes attributes )
    {
        this.parent = parent;
        this.name = name;
        this.startTagElementName = "<" + name + ">";
        this.endTagElementName = "</" + name + ">";
        this.attributes = attributes;
        this.startTagElement = new BufferedStartTagElement( this );
        this.endTagElement = new BufferedEndTagElement( this );
        this.currentTagElement = startTagElement;
        this.reseted = false;
    }

    /**
     * Returns true if the given name match the name of this element and false otherwise.
     * 
     * @param name
     * @return
     */
    public boolean match( String name )
    {
        if ( name == null )
        {
            return false;
        }
        return name.equals( this.name );
    }

    /**
     * Returns the buffer of the start tag element.
     * 
     * @return
     */
    public BufferedStartTagElement getStartTagElement()
    {
        return startTagElement;
    }

    /**
     * Returns the buffer of the end tag element.
     * 
     * @return
     */
    public BufferedEndTagElement getEndTagElement()
    {
        return endTagElement;
    }

    /**
     * Set content on the before start tag element.
     * 
     * @param before
     */
    public void setContentBeforeStartTagElement( String before )
    {
        this.startTagElement.setBefore( before );
    }

    /**
     * Set content on the after end tag element.
     * 
     * @param before
     */
    public void setContentAfterEndTagElement( String after )
    {
        this.endTagElement.setAfter( after );
    }

    /**
     * Returns the parent buffered element of this element.
     */
    public BufferedElement getParent()
    {
        return parent;
    }

    /**
     * Returns the current tag element (start or end).
     * 
     * @return
     */
    private BufferedTagElement getCurrentTagElement()
    {
        return currentTagElement;
    }

    /**
     * Write the content of this element in the given writer.
     */
    public void save( Writer writer )
        throws IOException
    {
        // Write start tag element content
        getStartTagElement().save( writer );
        // Write end tag element content
        getEndTagElement().save( writer );
    }

    /**
     * Add a savable region in the current tag element.
     */
    public void addRegion( ISavable region )
    {
        getCurrentTagElement().addRegion( region );
    }

    /**
     * Returns false
     */
    public boolean isString()
    {
        return getCurrentTagElement().isString();
    }

    /**
     * Append content in the current tag element.
     */
    public void append( String content )
    {
        getCurrentTagElement().append( content );
    }

    /**
     * Append content in the current tag element.
     */
    public void append( char[] ch, int start, int length )
    {
        getCurrentTagElement().append( ch, start, length );
    }

    /**
     * Append content in the current tag element.
     */
    public void append( char c )
    {
        getCurrentTagElement().append( c );
    }

    /**
     * Reset the whole content of the element.
     */
    public void reset()
    {
        startTagElement.reset();
        endTagElement.reset();
        this.reseted = true;
    }

    public boolean isReseted()
    {
        return reseted;
    }

    /**
     * Remove the collection of element.
     * 
     * @param elements
     */
    public void removeAll( Collection<BufferedElement> elements )
    {
        BufferedTagElement tagElement = null;
        for ( BufferedElement element : elements )
        {
            // remove start tag element
            tagElement = element.getStartTagElement();
            if ( tagElement != null )
            {
                getStartTagElement().regions.remove( tagElement );
            }
            // remove end tag element
            tagElement = element.getEndTagElement();
            if ( tagElement != null )
            {
                getStartTagElement().regions.remove( tagElement );
            }
        }
    }

    @Override
    public String toString()
    {
        StringWriter writer = new StringWriter();
        try
        {
            save( writer );
        }
        catch ( IOException e )
        {
            // Do nothing
        }
        return writer.toString();
    }

    /**
     * Returns the owner element.
     */
    public BufferedElement getOwnerElement()
    {
        return this;
    }

    /**
     * Returns the parent element of this element which match the given name.
     * 
     * @param element
     * @param name
     * @return
     */
    public BufferedElement findParent( String name )
    {
        return findParent( this, name );
    }

    /**
     * Returns the parent element of the given element which match the given name.
     * 
     * @param element
     * @param name
     * @return
     */
    public BufferedElement findParent( BufferedElement element, String name )
    {
        if ( element == null )
        {
            return null;
        }
        if ( element.match( name ) )
        {
            return element;
        }
        return findParent( element.getParent(), name );
    }

    /**
     * Returns the children element of this element which match the given name.
     * 
     * @param name
     * @return
     */
    public List<BufferedElement> findChildren( String name )
    {
        return findChildren( this, name );
    }

    /**
     * Returns the children element of the given element which match the given name.
     * 
     * @param element
     * @param name
     * @return
     */
    public List<BufferedElement> findChildren( BufferedElement element, String name )
    {
        List<BufferedElement> elements = new ArrayList<BufferedElement>();
        List<ISavable> regions = element.getStartTagElement().regions;
        findChildren( regions, name, elements );
        return elements;
    }

    /**
     * Returns the first child element of the given element which match the given name and null otherwise.
     * 
     * @param name
     * @return
     */
    public BufferedElement findFirstChild( String name )
    {
        return findFirstChild( this, name );
    }

    /**
     * Returns the first child element of this element which match the given name and null otherwise.
     * 
     * @param name
     * @return
     */
    public BufferedElement findFirstChild( BufferedElement element, String name )
    {
        List<BufferedElement> elements = findChildren( element, name );
        if ( elements.size() > 1 )
        {
            return elements.get( 0 );
        }
        return null;
    }

    /**
     * Populate list elements with children element which match the given name.
     * 
     * @param regions
     * @param name
     * @param elements
     */
    private void findChildren( List<ISavable> regions, String name, List<BufferedElement> elements )
    {
        for ( ISavable region : regions )
        {
            if ( region instanceof IBufferedRegion )
            {
                IBufferedRegion r = (IBufferedRegion) region;
                if ( r.getOwnerElement().match( name ) )
                {
                    elements.add( r.getOwnerElement() );
                }
                if ( r instanceof BufferedRegion )
                {
                    findChildren( ( (BufferedRegion) r ).regions, name, elements );
                }
            }
        }
    }

    /**
     * Set the current buffer with start tag element.
     */
    public void start()
    {
        this.currentTagElement = startTagElement;
    }

    /**
     * Set the current buffer with end tag element.
     */
    public void end()
    {
        this.currentTagElement = endTagElement;
    }

    /**
     * Returns true if current buffer is end tag element and false otherwise.
     * 
     * @return
     */
    public boolean isEnded()
    {
        return this.currentTagElement == endTagElement;
    }

    /**
     * Returns the name of this start tag element (ex : <w:t>).
     * 
     * @return
     */
    public String getStartTagElementName()
    {
        return startTagElementName;
    }

    /**
     * Returns the name of this end tag element (ex : </w:t>).
     * 
     * @return
     */
    public String getEndTagElementName()
    {
        return endTagElementName;
    }

    /**
     * Set text content for this element.
     * 
     * @param content
     */
    public void setTextContent( String content )
    {
        if ( isEnded() )
        {
            // end tag element is parsed (ex: <w:t>XXXX</w:t>), reset the buffer
            // and rebuild the buffer with the new content.
            reset();
            getStartTagElement().append( getStartTagElementName() );
            getStartTagElement().append( content );
            getEndTagElement().append( getEndTagElementName() );
        }
        else
        {
            // end tag element is not parsed (ex: <w:t>) append teh content.
            getCurrentTagElement().append( content );
        }
    }

    /**
     * Returns the text content for this element.
     * 
     * @return
     */
    public String getTextContent()
    {
        // get content of the element (ex : <w:t>XXXX</w:t>)
        StringWriter writer = new StringWriter();
        try
        {
            save( writer );
        }
        catch ( IOException e )
        {

            e.printStackTrace();
        }
        // remove the start/end tag ex : XXXX)
        String textContent = writer.toString();
        if ( textContent.startsWith( getStartTagElementName() ) )
        {
            textContent = textContent.substring( getStartTagElementName().length(), textContent.length() );
        }
        if ( textContent.endsWith( getEndTagElementName() ) )
        {
            textContent = textContent.substring( 0, textContent.length() - getStartTagElementName().length() - 1 );
        }
        return textContent;
    }

    /**
     * Returns the static SAX attributes.
     * 
     * @return
     */
    public Attributes getAttributes()
    {
        return attributes;
    }

    /**
     * Register dynamic attributes if needed.
     */
    public void registerDynamicAttributes()
    {
        if ( dynamicAttributes == null )
        {
            return;
        }
        for ( BufferedAttribute attribute : dynamicAttributes )
        {
            getCurrentTagElement().addRegion( attribute );
        }
    }

    /**
     * Set dynamic attribute.
     * 
     * @param name
     * @param value
     * @return
     */
    public BufferedAttribute setAttribute( String name, String value )
    {
        if ( dynamicAttributes == null )
        {
            dynamicAttributes = new ArrayList<BufferedAttribute>();
        }
        BufferedAttribute attribute = new BufferedAttribute( this, name, value );
        dynamicAttributes.add( attribute );
        return attribute;
    }

    public String getName()
    {
        return name;
    }

    public String getInnerText()
    {
        StringWriter writer = new StringWriter();
        List<ISavable> regions = startTagElement.regions;
        boolean startTagParsing = true;
        for ( ISavable region : regions )
        {
            if ( startTagParsing )
            {
                if ( region instanceof BufferedStartTagElement )
                {
                    startTagParsing = false;
                }
            }
            if ( !startTagParsing )
            {
                try
                {
                    region.save( writer );
                }
                catch ( IOException e )
                {
                    // Should never thrown.
                }
            }
        }
        return writer.toString();
    }

    public void setInnerText( String innerText )
    {
        List<ISavable> regionsToAdd = new ArrayList<ISavable>();
        List<ISavable> regions = startTagElement.regions;
        boolean startTagParsing = true;
        for ( ISavable region : regions )
        {
            startTagParsing = !( region instanceof BufferedStartTagElement );
            if ( startTagParsing )
            {
                regionsToAdd.add( region );
            }
            else
            {
                break;
            }
        }
        startTagElement.reset();
        startTagElement.regions.addAll( regionsToAdd );
        startTagElement.append( innerText );

    }

    public String get( String key )
    {
        if ( data == null )
        {
            return null;
        }
        return data.get( key );
    }

    public void put( String key, String value )
    {
        if ( data == null )
        {
            data = new HashMap<String, String>();
        }
        data.put( key, value );
    }

}
